package lapsnake.model;

import java.awt.Component;
import java.beans.PropertyVetoException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import lapsnake.model.bounceback.ScriptNode;
import lapsnake.model.bounceback.StringNode;
import lapsnake.model.data.ChildField;
import lapsnake.model.data.Property;
import lapsnake.model.dnd.ActionNodePanel;
import lapsnake.model.dnd.FieldPanel;
import lapsnake.model.dnd.NodePanel;
import lapsnake.model.dnd.StmtPanel;
import lapsnake.model.expr.LapName;
import lapsnake.model.misc.LapAlias;
import lapsnake.py.ast.Value;
import lapsnake.ui.JTreeLabel;

import org.python.antlr.*;
import org.python.antlr.ast.alias;


/**Ancestor for all grammar helper classes*/
public abstract class LapNode implements ScriptNode {
	
	/**Annotation for ChildField fields which should not be included in {@link LapNode#getChildFields()}*/
	@Retention(RetentionPolicy.RUNTIME) 
	protected @interface Ignore {}
	/**Annotation for ChildField fields which should be on a new line*/
	@Retention(RetentionPolicy.RUNTIME) 
	protected @interface NewRow {}
	
	/**The PyAST instance of this tree.*/
	public final PyAST pyAST;
	/**The field on the parent where this node is registered.*/
	@Ignore public final ChildField<?> parentField;
	/**Whether this node has been registered on the parent.*/
	private boolean isRegistered = false;
	/**If this node was created from an AST node, then this field will not be null. 
	 * It should only be used internally; 
	 * The information will become invalid as the tree is modified.*/
	private final PythonTree origNode;
	/**When building the tree from AST nodes, this will be set to the start/end position of the node in the text.
	 * This is necessary because {@link #origNode} will only report the start/end positions of some unspecified tokens.*/
	private int origStartIndex, origEndIndex;
	/**The list of nodes used to reconstruct the python script from the tree with minimal information loss.*/
	private ArrayList<ScriptNode> scriptNodes;
	
	//????
	private boolean elif = false;
	
	public <X extends LapNode> LapNode(PyAST pyAST, ChildField<X> parentField, PythonTree node) throws PropertyVetoException {
		assert pyAST != null;
		this.pyAST = pyAST;
		assert parentField != null || this instanceof LapModule : this.getClass().getSimpleName()+" cannot have a null parent!";
		this.parentField = parentField;
		this.origNode = node;
	}
	
	/**Called from bottom(leaf) to top(root) when building custom nodes.
	 * @throws PropertyVetoException If there was a veto listener which prevented the adding of this node to the parent field.*/
	protected final void setTextNodes(ScriptNode... nodes) throws PropertyVetoException
	{
		scriptNodes = new ArrayList<>(Arrays.asList(nodes));
		if(!isRegistered)
			register(parentField);
		pyAST.subTreeChanged(this);
	}
	/**Call when changing the internal value of nodes with no children. This is not a subsititute for {@link #setTextNodes(ScriptNode...)}.*/
	protected final void changeNodeText(String text)
	{
		scriptNodes.clear();
		scriptNodes.add(new StringNode(text));
		pyAST.subTreeChanged(this);
	}
	
	protected boolean isRegistered() 
	{
		return isRegistered;
	}
	
	
	/**Called from bottom(leaf) to top(root) when building a tree from an AST. 
	 * This must not be called upon return from the AST-related constructor, and only then.
	 * @throws PropertyVetoException If there was a veto listener which prevented the adding of this node to the parent field.*/
	protected void buildTextNodes(PythonTree node) throws PropertyVetoException
	{
		scriptNodes = new ArrayList<>();
		LapNode[] children = getChildren();
		Arrays.sort(children, new Comparator<LapNode>()
			{
			@Override
			public int compare(LapNode a, LapNode b)
			{
				return a.origStartIndex-b.origStartIndex;
			}
			});
		
		//Note: getCharStartIndex returns the start of some token X, where X is unspecified, possibly null. This is stupid. 
		//To fix, we have to use the start location of the first located token, which may be on this node or any of the child nodes.
		origStartIndex = origNode.getCharStartIndex();
		if(origStartIndex < 0) {
			origStartIndex = Integer.MAX_VALUE;
		}
		for(int i = 0; i<children.length; i++)
		{
			int cStart = children[i].origStartIndex;
			if(cStart < origStartIndex && cStart > 0)
				origStartIndex = cStart;
		}
		
		LapNode last = this;
		
		int textPos = origStartIndex;
		assert pyAST != null;
		assert pyAST.getScript() != null;
		assert pyAST.getScript().length() == 0 || (textPos >= 0 && textPos < pyAST.getScript().length()) : 
			getClass().getSimpleName()+": textPos out of range: "+textPos+"/"+pyAST.getScript().length();
		for(int i = 0; i<children.length; i++)
		{
			if(children[i] == last)
				throw new IllegalArgumentException(getClass().getSimpleName()+"("+origNode+") contains duplicates at "+(i-1)+", "+i);
			int cStart = children[i].origStartIndex;
			assert cStart >= 0 && cStart < pyAST.getScript().length() : getClass().getSimpleName()+": "
				+ "cStart out of range: "+cStart+"/"+pyAST.getScript().length();
			assert cStart>=textPos: getClass().getSimpleName()+": "
				+ "Child node "+children[i]+" begins before previous node "+
				(last==this?"itself("+last.getClass().getSimpleName()+")":last)+" ends: "+textPos+" -> "+cStart+"...\n"
					+ "Location: "+children[i].origNode.getCharStartIndex()+" -> "+children[i].origNode.getCharStopIndex()+
					" = "+pyAST.getScript().substring(children[i].origNode.getCharStartIndex(), children[i].origNode.getCharStopIndex())+
					" line "+origNode.getLine()+" pos. "+origNode.getCharPositionInLine()+" child index: "+i+"\n"+
					"children = "+Arrays.toString(children)+ "\n" +
					"and last started at: "+last.origNode.getCharStartIndex();
			String middle = pyAST.getScript().substring(textPos, cStart);
			
			Pattern nonBlankPat = Pattern.compile("\\S");
			String[] lines = middle.split("\n", -1);
			
			if(lines.length > 1)
				scriptNodes.add(new StringNode(lines[0]+"\n"));
			else
				scriptNodes.add(new StringNode(lines[0]));
			
			for (int j = 1; j < lines.length; j++)
			{
				String line = lines[j];
				Matcher nonBlankMat = nonBlankPat.matcher( line );
				if(nonBlankMat.find())
				{
					line = line.substring(nonBlankMat.start());
					if (j < lines.length-1)
						line += "\n";
				}
				else if (j < lines.length-1)
					line = "\n";
				else
					line = null;
				
				if(line != null)
				{
					if(j == lines.length-1)
					{
						int commentIndex = line.indexOf('#');
						int elifIndex = line.indexOf("elif");
						if(elifIndex >= 0 && (elifIndex < commentIndex || commentIndex == -1))
						{
							children[i].elif(line.substring(elifIndex+2));
							line = line.substring(0, elifIndex+2);
						}
					}
					
					scriptNodes.add(new StringNode(line));
				}
			}
			/*int lastnl = middle.lastIndexOf('\n');
			if(lastnl != -1)
			{
				String lastLine = middle.substring(lastnl+1);
				if(!lastLine.contains(":"))
				{
					String discard = middle.substring(lastnl+1);
					int commentIndex = discard.indexOf('#');
					int elifIndex = discard.indexOf("elif");
					if(elifIndex >= 0 && (elifIndex < commentIndex || commentIndex == -1))
					{
						children[i].elif(discard.substring(elifIndex));
					}
					middle = middle.substring(0, lastnl+1);
					
				}
			}
			
			scriptNodes.add(new StringNode(middle));*/
			scriptNodes.add(children[i]);
			textPos = children[i].origEndIndex;
			assert textPos > 0 && textPos < pyAST.getScript().length() : this+": textPos out of range: "+textPos+"/"+pyAST.getScript().length();
			last = children[i];
		}
		
		//Note: getCharStopIndex only returns the end of some token X, where X is unspecified, possibly null. This is downright retarded. 
		//To fix, we have to use the end location of the last located token, which may be on this node or any of the child nodes.
		if(pyAST.getScript().length() == 0)
			origEndIndex = 0;
		else
		{
			origEndIndex = Math.max(textPos, origNode.getCharStopIndex());
			assert (origEndIndex > 0 && origEndIndex < pyAST.getScript().length()) : 
				"origEndIndex out of range: "+origEndIndex+"/"+pyAST.getScript().length();
		}
		
		String endString = pyAST.getScript().substring(textPos, origEndIndex);
		if(endString.endsWith("\n"))
		{
			//Some nodes eat up the newline character for some reason, need to export it to the parent node
			endString = endString.substring(0, endString.length()-1);
			origEndIndex--;
		}
		scriptNodes.add(new StringNode(endString));
		assert !isRegistered;
		register(parentField);
	}
	private void elif(String prefix)
	{
		elif = true;
		ScriptNode first = scriptNodes.get(0);
		if(first instanceof StringNode)
			scriptNodes.set(0, new StringNode(prefix+((StringNode)first).string));
		else
			scriptNodes.add(0, new StringNode(prefix));
	}

	/**Prepend the indicated string to the first ScriptNode on this node. 
	 * This method should only be used when constructing the tree from the AST.*/
	/*private void prepend(String prefix)
	{
		StringNode left = (StringNode) scriptNodes.get(0);
		scriptNodes.set(0, new StringNode(prefix+left.string));
	}*/
	/**Register this node on the parent node. This is called upon the first call to {@link #buildTextNodes(PythonTree)} or {@link #setTextNodes(ScriptNode...)}*/
	private<X extends LapNode> void register(ChildField<X> field) throws PropertyVetoException
	{
		isRegistered = true;
		if(field == null)
		{
			assert this instanceof LapModule : this+" must be assigned a parent!!!";
			return;
		}
		try
		{
			if(field.getType() == LapAlias.class && this instanceof LapName) //Bug in compiler causes alias nodes to become name nodes
			{
				String id = ((LapName)this).identifier.get();
				pyAST.warning(getClass().getSimpleName()+" node("+origNode+"="+id+") encountered at line"+
					origNode.getLine()+", pos. "+origNode.getCharPositionInLine()+", but "+field+" expects an Alias node...");
				@SuppressWarnings("unchecked")
				ChildField<? super LapAlias> aliasField = (ChildField<? super LapAlias>)field;
				alias synthesizedNode = new alias(origNode.getToken(), origNode.asString(), null);
				synthesizedNode.setCharStartIndex(origNode.getCharStartIndex());
				synthesizedNode.setCharStopIndex(origNode.getCharStopIndex());
				new LapAlias(pyAST, aliasField, synthesizedNode);
			}
			else
				field.add(field.cast(this), this, true);
		}
		catch(IllegalArgumentException | NullPointerException | ClassCastException e)
		{
			throw new RuntimeException("Internal error: Trying to add "+this+" to "+parentField, e);
		}
	}
	/**Write the part of the textual python script represented by this subtree to the indicated string builder.
	 * For example, if this is a LapAssign node then sb might be appended by "a = 8".*/
	@Override
	public boolean appendScript(StringBuilder sb, boolean startOfLine, int indentLevel)
	{
		assert isRegistered : "Error: "+this+" has not been registered on its parent!";
		if(!(this instanceof LapModule) && parentField.isIndented() && !elif)
			indentLevel++;
		for(ScriptNode n : scriptNodes)
		{
			startOfLine = n.appendScript(sb, startOfLine, indentLevel);
		}
		return startOfLine;
	}
	
	/**Append a verbose string representing this subtree to the indicated stringbuilder.
	 * This will include the part of the script belonging to each node.*/
	@Override
	public void visualizeScript(StringBuilder sb, int ident)
	{
		assert isRegistered;
		String idt = new String(new char[ident]).replaceAll("\0", "  ");
		sb.append(idt);
		sb.append(getClass().getSimpleName());
		sb.append("{\n");
		for(ScriptNode n : scriptNodes)
		{
			n.visualizeScript(sb, ident+1);
		}
		sb.append(idt);
		sb.append("}\n");
	}
	
	/**Return an UI component which can be nested into, or become an action block in the class editor.*/
	public final NodePanel<?> getCompactUI(StmtPanel rootPanel)
	{
		assert isRegistered;
		ActionNodePanel<LapNode> ui = new ActionNodePanel<LapNode>(rootPanel, this);
		populateCompactUI(ui);
		return ui;
	}
	
	
	protected void populateCompactUI(ActionNodePanel<LapNode> ui)
	{
		ChildField<?>[] fields = getChildFields();
		for (int i = 0; i < fields.length; i++)
		{
			ChildField<?> f = fields[i];
			if(f.hasNLAnnotation())
				ui.newLine();
			ui.addLow(getFillerComponent(i, f));
			ui.addHigh(new FieldPanel(ui, f));
		}
	}

	protected Component getFillerComponent(int childIndex, ChildField<?> childField)
	{
		if(childIndex == 0)
		{
			JTreeLabel lab = new JTreeLabel(NodeRegister.getReadableName(getClass()), false);
			return lab;
		}
		
		return null;
	}

	/**Return all the fields which might contain child nodes, in the declaration order. 
	 * Note that this may not match the text order in the python script.*/
	public final ChildField<?>[] getChildFields()
	{
		try
		{
			ArrayList<ChildField<?>> list = new ArrayList<>();
			for(Field f : getClass().getDeclaredFields())
			{
				//Ignore fields marked with NotChild
				if(f.getAnnotation(Ignore.class) != null)
					continue;
				
				if(ChildField.class.isAssignableFrom(f.getType()))
				{
					f.setAccessible(true);
					Object val = f.get(this);
					if(val == null)
						throw new IllegalStateException("ChildField slot: "+f.getName()+" on object: "+this+" cannot be null!");
					
					ChildField<?> cf = ChildField.class.cast(val);
					if(f.getAnnotation(NewRow.class) != null)
						cf.setNLAnnotation();
					list.add(cf);
				}
			}
			return list.toArray(new ChildField[list.size()]);
		}
		catch (IllegalArgumentException | IllegalAccessException e)
		{
			throw new RuntimeException(e);
		}
	}

	/**Return all properties that might change, in the declaration order.
	 * Note that this does not include ChildFields.*/
	public final Property<?>[] getProperties()
	{
		try
		{
			ArrayList<Property<?>> list = new ArrayList<>();
			for(Field f : getClass().getDeclaredFields())
			{
				//Ignore fields marked with Ignore
				if(f.getAnnotation(Ignore.class) != null)
					continue;
				
				if(Property.class.isAssignableFrom(f.getType()))
				{
					f.setAccessible(true);
					Object val = f.get(this);
					if(val == null)
						throw new IllegalStateException("Property slot: "+f.getName()+" on object: "+this+" cannot be null!");
					
					list.add(Property.class.cast(val));
				}
			}
			return list.toArray(new Property[list.size()]);
		}
		catch (IllegalArgumentException | IllegalAccessException e)
		{
			throw new RuntimeException(e);
		}
	}
	/**Return all the child nodes, lexically ordered by:<br> 
	 * First, the order of child fields and <br>
	 * Secondly, the order they were added.*/
	public final LapNode[] getChildren()
	{
		ArrayList<LapNode> list = new ArrayList<>();
		for(ChildField<?> field : getChildFields())
		{
			for(LapNode c : field.toArray())
				list.add(c);
		}
		return list.toArray(new LapNode[list.size()]);
	}
	/**Return a representation of this subtree, truncated to 32 characters, on a single line(probably).*/
	@Override
	public final String toString()
	{
		assert isRegistered;
		StringBuilder sb = new StringBuilder();
		buildTreeString(sb, 0, false);
		if(sb.length() > 32)
		{
			sb.setLength(29);
			sb.append("...");
		}
		return sb.toString();
	}
	/**Append a sort-of-readable representation of this subtree to the indicated stringbuilder, starting at the indentation level.
	 * If nl is false, then the string will be on a single line, as long as there are no multi-line strings.*/
	public final void buildTreeString(StringBuilder target, int ident, boolean nl)
	{
		assert isRegistered;
		if(nl)
		{
			String idt = new String(new char[ident]).replaceAll("\0", "  ");
			target.append(idt);
		}
		target.append(getClass().getSimpleName());
		if(this instanceof Value)
		{
			target.append('(');
			target.append(((Value<?>)this).value().toString());
			target.append(')');
		}
		LapNode[] children = getChildren();
		if(nl)
			target.append('\n');
		else if(children.length > 0)
			target.append('{');
		if(children.length > 0)
		{
			for (int i = 0; i < children.length; i++)
			{
				if(i != 0 && !nl)
					target.append(", ");
				LapNode c = children[i];
				c.buildTreeString(target, ident+1, nl);
			}
			if(!nl)
				target.append('}');
		}
	}
}
